/*
 *	Filename:		ado.cpp
 *	Author:			Luke Douglas
 *	Date:			11/10/01
 *	Copyright:		(c) 2001-2 Realtime Gaming
 *
 *	History:		luke: 11/10/01	Created
 *				    luke: 11/12/01	Spawned off header file
 *					luke: 1/16/02	Cleaned up
 *					luke: 2/3/02	Moved to mbcommon and renamed
 */

#include <atlbase.h>
#include "ado.h"


// Helper function to print a verbose COM error
void ADO::PrintComError(_com_error* pError, LPWSTR szwPrefix)
{
	ERRORX(L"%s: Error 0x%08x\n\tMessage: %s\n\tSource: %s\n\tDescription: %s\n",
		   szwPrefix,
		   pError->Error(),
		   pError->ErrorMessage(),
		   (LPWSTR)_bstr_t(pError->Source()),
		   (LPWSTR)_bstr_t(pError->Description()));
}


// ADO constructor: create pCon, pCmd, and pPrm and initialize state
ADO::ADO()
{
	pCon.CreateInstance(__uuidof(ADODB::Connection));
	pCmd.CreateInstance(__uuidof(ADODB::Command));
	pPrm.CreateInstance(__uuidof(ADODB::Parameter));

	bConnected = FALSE;
	iDelay = 0;
}


// There's no dynamic deallocation to do, so the destructor is simple: just disconnect
ADO::~ADO()
{
	Disconnect();
}


// Import the appropriate configuration options to ADO
// Note that pointers, not data, are copied, so don't delete the strings until done!
BOOL ADO::Configure(LPWSTR szwConnectionString, LPWSTR szwUsername, LPWSTR szwPassword,
					int iMinDelay, int iMaxDelay, int iDelayStep)
{
	m_szwConnectionString = szwConnectionString;
	m_szwUsername = szwUsername;
	m_szwPassword = szwPassword;
	m_iMinDelay = iMinDelay;
	m_iMaxDelay = iMaxDelay;
	m_iDelayStep = iDelayStep;

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Open a connection to the database and, if successful, point the command object at the new connection
BOOL ADO::Connect()
{
	// Don't attempt to reconnect if already connected
	if (bConnected)
		return TRUE;

	try
	{
		pCon->Open(m_szwConnectionString, m_szwUsername, m_szwPassword, ADODB::adOpenUnspecified);
		pCmd->ActiveConnection = pCon;
		bConnected = TRUE;
	}
	catch (_com_error &e)
	{
		ADO::PrintComError(&e, L"ADO::Connect");
		return FALSE;
	}
	catch (...)
	{
		ERRORX(L"ADO::Connect: Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Close a connection to the database
BOOL ADO::Disconnect()
{
	// Don't attempt to disconnect if already disconnected
	if (!bConnected)
		return TRUE;

	try
	{
		pCon->Close();
		bConnected = FALSE;
	}
	catch (_com_error)
	{
		// ADO::DebugComError(&e, L"ADO::Disconnect");
		return FALSE;
	}
	catch (...)
	{
		// DEBUG(L"ADO::Disconnect: Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Reset the internal command object for the execution of a new stored procedure. Direct the
// command object to execute szwSProc with a parameter list starting with the five provided
// parameters
BOOL ADO::NewCommand(LPWSTR szwSProc, LPWSTR szwCasino, LPWSTR szwRemoteHost, LPWSTR szwCheckCasino, int iCheckNo)
{
	int iRemoteHost, iCheckCasino;

	try
	{
		pCmd->CommandText = szwSProc;
		pCmd->CommandType = ADODB::adCmdStoredProc;

		// Remove all parameters (Refresh() doesn't do it)
		for (int i = pCmd->Parameters->Count; i > 0; i--)
			pCmd->Parameters->Delete((long)i-1L);

		// The first (implicit) parameter must handle the stored procedure's return value
		pPrm = pCmd->CreateParameter(L"", ADODB::adInteger, ADODB::adParamReturnValue, 4, &vtMissing);
		pCmd->Parameters->Append(pPrm);

		// Pass in the name of the casino executing the transaction
		if (szwCasino == NULL || szwCasino[0] == '0')
			return FALSE;
		pPrm = pCmd->CreateParameter(L"", ADODB::adVarWChar, ADODB::adParamInput, wcslen(szwCasino), &vtMissing);
		pPrm->Value = szwCasino;
		pCmd->Parameters->Append(pPrm);

		// Pass in the hostname of the casino server (this would be used to initiate a DTC transaction)
		// *** For now: this is allowed to be null
		iRemoteHost = szwRemoteHost ? wcslen(szwRemoteHost) : 2;
		pPrm = pCmd->CreateParameter(L"", ADODB::adVarWChar, ADODB::adParamInput, iRemoteHost, &vtMissing);
		pPrm->Value = szwRemoteHost ? szwRemoteHost : L"";
		pCmd->Parameters->Append(pPrm);

		// Pass in the name of the casino that signed the check, if one exists
		// *** For now: this is also allowed to be null
		iCheckCasino = szwCheckCasino ? wcslen(szwCheckCasino) : 2;
		pPrm = pCmd->CreateParameter(L"", ADODB::adVarWChar, ADODB::adParamInput, iCheckCasino, &vtMissing);
		pPrm->Value = szwCheckCasino ? szwCheckCasino : L"";
		pCmd->Parameters->Append(pPrm);

		// Pass in the check number, if one exists, or 0, if one doesn't
		pPrm = pCmd->CreateParameter(L"", ADODB::adInteger, ADODB::adParamInput, 4, &vtMissing);
		pPrm->Value = (long)iCheckNo;
		pCmd->Parameters->Append(pPrm);
	}
	catch (_com_error)
	{
		// ADO::DebugComError(&e, L"ADO::NewCommand");
		return FALSE;
	}
	catch (...)
	{
		// DEBUG(L"ADO::NewCommand: Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Append an nvarchar parameter to the current command, enabling it for output if bOutput is set
BOOL ADO::AddParameter(LPWSTR szwParam, BOOL bOutput)
{
	ADODB::ParameterDirectionEnum pdeDir = bOutput ? ADODB::adParamInputOutput : ADODB::adParamInput;

	try
	{
		pPrm = pCmd->CreateParameter(L"", ADODB::adVarWChar, pdeDir, wcslen(szwParam), &vtMissing);
		pPrm->Value = szwParam;
		pCmd->Parameters->Append(pPrm);
	}
	catch (_com_error)
	{
		// ADO::DebugComError(&e, L"ADO::AddParameter (szw)");
		return FALSE;
	}
	catch (...)
	{
		// DEBUG(L"ADO::AddParameter (szw): Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Append a money parameter to the current command, enabling it for output if bOutput is set
BOOL ADO::AddParameter(SQLMONEY amount, BOOL bOutput)
{
	ADODB::ParameterDirectionEnum pdeDir = bOutput ? ADODB::adParamInputOutput : ADODB::adParamInput;
	CY c;

	try
	{
		pPrm = pCmd->CreateParameter(L"", ADODB::adCurrency, pdeDir, 8, &vtMissing);
		c.int64 = amount;
		pPrm->Value = c;
		pCmd->Parameters->Append(pPrm);
	}
	catch (_com_error)
	{
		// ADO::DebugComError(&e, L"ADO::AddParameter (money)");
		return FALSE;
	}
	catch (...)
	{
		// DEBUG(L"ADO::AddParameter (money): Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


BOOL ADO::AddParameter(int iParam, BOOL bOutput)
{
	ADODB::ParameterDirectionEnum pdeDir = bOutput ? ADODB::adParamInputOutput : ADODB::adParamInput;

	try
	{
		pPrm = pCmd->CreateParameter(L"", ADODB::adInteger, pdeDir, 4, &vtMissing);
		pPrm->Value = (long)iParam;
		pCmd->Parameters->Append(pPrm);
	}
	catch (_com_error)
	{
		// ADO::DebugComError(&e, L"ADO::AddParameter (int)");
		return FALSE;
	}
	catch (...)
	{
		// DEBUG(L"ADO::AddParameter (int): Unhandled exception\n");
		return FALSE;
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Internal execute method: wrap execution results into a bool and an error code
BOOL ADO::_Execute(int* piReturn)
{
	try
	{
		pCmd->Execute(&vtMissing, &vtMissing, -1);
		// The procedure executed successfully: copy its return code to piError
		*piReturn = (long)pCmd->Parameters->Item[_variant_t(0L)]->Value;

		goto success;
	}
	catch (_com_error &e)
	{
		PrintComError(&e, L"_Execute");
		goto error;
	}
	catch (...)
	{
		DEBUGMSG(L"_Execute: Unhandled exception\n");
		goto error;
	}

error:
	// The procedure failed to execute; copy the error code to piError
	// Make sure that the errors collection is filled before attempting to access it
	if (pCon->Errors->Count == 0)
		*piReturn = 0;
	else
	{
		*piReturn = (long)pCon->Errors->Item[_variant_t(0L)]->NativeError;

		// An error code of 0 indicates an (implicit) connection failure; make it more
		// obvious by setting piError instead to 911, the explicit connection failure code
		if (*piReturn == 0)
			*piReturn = 911;
		// SQL error 3621 means "The statement has been terminated". A more useful error code
		// is stored below it in the errors collection.
		else if (*piReturn == 3621 && pCon->Errors->Count > 1)
			*piReturn = pCon->Errors->Item[_variant_t(1L)]->NativeError;
	}
	return FALSE;

success:
	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}


// Execute the internal command object
// NOTE: The size of pAmounts is determined by the maximum transfer count of
// the pub_AccountCompoundTransfer stored procedure
BOOL ADO::Execute(int* piReturnCode, int* piNumAmounts, SQLMONEY pAmounts[8])
{
	BOOL bAlreadyReset = FALSE;
	int iReturn;

	// Zero out outgoing parameters for consistency
	*piReturnCode = 0;
	*piNumAmounts = 0;
	for (int i = 0; i < 8; i++)
		memset(&pAmounts[i], 0, sizeof(SQLMONEY));

	while (1)
	{
		if (!Connect())
		{
			ERRORX(L"ADO::Execute: Unable to connect\n");
			return FALSE;
		}

		Sleep(iDelay);
		if (_Execute(&iReturn))
		{
			// Successful execution: decrease the delay interval
			iDelay = (iDelay <= m_iMinDelay) ? 0 : iDelay/m_iDelayStep;

			// Return the return code to the caller
			*piReturnCode = iReturn;

			// If the command was successful, look at each parameter to pCmd. If it
			// contains a currency value and is an output parameter, then its value
			// is meant to be returned to the client; put it into pAmounts
			if (iReturn == 0)
			{
				int j = 0;

				for (i = 0; i < pCmd->Parameters->Count; i++)
				{
					if (pCmd->Parameters->Item[(long)i]->Type == ADODB::adCurrency &&
						pCmd->Parameters->Item[(long)i]->Direction == ADODB::adParamInputOutput)
					{
						CY c = pCmd->Parameters->Item[(long)i]->Value;
						pAmounts[j++] = c.int64;
						*piNumAmounts += 1;
					}
				}
			}

			// 2670f929ce8c93af815ba0ae8fd591ff
			return TRUE;
		}
		else
		{
			if (iReturn == 0 || iReturn == 547)
			{
				// An unidentified error or a CHECK constraint can't be corrected. Return failure.
				*piReturnCode = iReturn;
				return FALSE;
			}
			else if (iReturn == 911)
			{
				// If a connection failure occurs, reset the connection and try to execute again.
				// (Signal a reset by setting iDelay to a value greater than the maximum delay.)
				iDelay = m_iMaxDelay + 1;
			}
			else
			{
				// If any other error occurs, increase the delay interval and try again
				iDelay = (iDelay == 0) ? m_iMinDelay : iDelay*m_iDelayStep;
			}
		}

		// If the delay interval exceeds the maximum, set it to zero, reset the connection,
		// and keep trying (unless the connection's already been reset once -- fail in this case)
		if (iDelay > m_iMaxDelay)
		{
			if (bAlreadyReset)
				return FALSE;
			else
			{
				// This disconnect ensures that the next command will create a fresh connection
				Disconnect();
				iDelay = 0;
				bAlreadyReset = TRUE;
			}
		}
	}

	// 2670f929ce8c93af815ba0ae8fd591ff
	return TRUE;
}
